/*
 * $Id: PaintUtils.java 4193 2012-06-27 19:42:05Z kschaefe $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.jdesktop.swingx.util;

import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.LinearGradientPaint;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.TexturePaint;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * A collection of utilities for working with Paints and Colors.
 *
 * @author Mark Davidson
 * @author joshua.marinacci@sun.com
 * @author Karl George Schaefer
 */
@SuppressWarnings("nls")
public class PaintUtils {
	public static final GradientPaint BLUE_EXPERIENCE = new GradientPaint(new Point2D.Double(0, 0), new Color(168, 204, 241),
			new Point2D.Double(0, 1), new Color(44, 61, 146));
	public static final GradientPaint MAC_OSX_SELECTED = new GradientPaint(new Point2D.Double(0, 0), new Color(81, 141, 236),
			new Point2D.Double(0, 1), new Color(36, 96, 192));
	public static final GradientPaint MAC_OSX = new GradientPaint(new Point2D.Double(0, 0), new Color(167, 210, 250),
			new Point2D.Double(0, 1), new Color(99, 147, 206));
	public static final GradientPaint AERITH = new GradientPaint(new Point2D.Double(0, 0), Color.WHITE,
			new Point2D.Double(0, 1), new Color(64, 110, 161));
	public static final GradientPaint GRAY = new GradientPaint(new Point2D.Double(0, 0), new Color(226, 226, 226),
			new Point2D.Double(0, 1), new Color(250, 248, 248));
	public static final GradientPaint RED_XP = new GradientPaint(new Point2D.Double(0, 0), new Color(236, 81, 81),
			new Point2D.Double(0, 1), new Color(192, 36, 36));
	public static final GradientPaint NIGHT_GRAY = new GradientPaint(new Point2D.Double(0, 0), new Color(102, 111, 127),
			new Point2D.Double(0, 1), new Color(38, 45, 61));
	public static final GradientPaint NIGHT_GRAY_LIGHT = new GradientPaint(new Point2D.Double(0, 0), new Color(129, 138, 155),
			new Point2D.Double(0, 1), new Color(58, 66, 82));

	// originally included in LinearGradientPainter
	public static final Paint ORANGE_DELIGHT = new LinearGradientPaint(new Point2D.Double(0, 0), new Point2D.Double(1, 0),
			new float[] { 0f, .5f, .51f, 1f }, new Color[] { new Color(248, 192, 75), new Color(253, 152, 6),
					new Color(243, 133, 0), new Color(254, 124, 0) });

	// originally included in LinearGradientPainter
	public static final Paint BLACK_STAR = new LinearGradientPaint(new Point2D.Double(0, 0), new Point2D.Double(1, 0),
			new float[] { 0f, .5f, .51f, 1f }, new Color[] { new Color(54, 62, 78), new Color(32, 39, 55),
					new Color(74, 82, 96), new Color(123, 132, 145) });

	private PaintUtils() {
	}

	/**
	 * Resizes a gradient to fill the width and height available. If the
	 * gradient is left to right it will be resized to fill the entire width. If
	 * the gradient is top to bottom it will be resized to fill the entire
	 * height. If the gradient is on an angle it will be resized to go from one
	 * corner to the other of the rectangle formed by (0,0 -> width,height).
	 *
	 * This method can resize java.awt.GradientPaint,
	 * java.awt.LinearGradientPaint, and the LinearGradientPaint implementation
	 * from Apache's Batik project. Note, this method does not require the
	 * MultipleGradientPaint.jar from Apache to compile or to run.
	 * MultipleGradientPaint.jar *is* required if you want to resize the
	 * LinearGradientPaint from that jar.
	 *
	 * Any paint passed into this method which is not a kind of gradient paint
	 * (like a Color or TexturePaint) will be returned unmodified. It will not
	 * throw an exception. If the gradient cannot be resized due to other errors
	 * the original paint will be returned unmodified. It will not throw an
	 * exception.
	 *
	 */
	public static Paint resizeGradient(Paint p, int width, int height) {
		if (p == null)
			return p;

		if (p instanceof GradientPaint) {
			GradientPaint gp = (GradientPaint) p;
			Point2D[] pts = new Point2D[2];
			pts[0] = gp.getPoint1();
			pts[1] = gp.getPoint2();
			pts = adjustPoints(pts, width, height);
			return new GradientPaint(pts[0], gp.getColor1(), pts[1], gp.getColor2(), gp.isCyclic());
		}

		if ("java.awt.LinearGradientPaint".equals(p.getClass().getName())
				|| "org.apache.batik.ext.awt.LinearGradientPaint".equals(p.getClass().getName())) {
			return resizeLinearGradient(p, width, height);
		}
		return p;
	}

	private static Paint resizeLinearGradient(Paint p, int width, int height) {
		try {
			Point2D[] pts = new Point2D[2];
			pts[0] = (Point2D) invokeMethod(p, "getStartPoint");
			pts[1] = (Point2D) invokeMethod(p, "getEndPoint");
			pts = adjustPoints(pts, width, height);
			float[] fractions = (float[]) invokeMethod(p, "getFractions");
			Color[] colors = (Color[]) invokeMethod(p, "getColors");

			Constructor<?> con = p.getClass().getDeclaredConstructor(Point2D.class, Point2D.class, new float[0].getClass(),
					new Color[0].getClass());
			return (Paint) con.newInstance(pts[0], pts[1], fractions, colors);
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return p;
	}

	private static Object invokeMethod(final Object p, final String methodName) throws NoSuchMethodException,
			InvocationTargetException, IllegalArgumentException, SecurityException, IllegalAccessException {
		Method meth = p.getClass().getMethod(methodName);
		return meth.invoke(p);
	}

	private static Point2D[] adjustPoints(Point2D[] pts, int width, int height) {
		Point2D start = pts[0];
		Point2D end = pts[1];

		double angle = calcAngle(start, end);
		double a2 = Math.toDegrees(angle);
		double e = 1;

		// if it is near 0 degrees
		if (Math.abs(angle) < Math.toRadians(e) || Math.abs(angle) > Math.toRadians(360 - e)) {
			start = new Point2D.Float(0, 0);
			end = new Point2D.Float(normalize(end.getX(), width), 0);
		}

		// near 45
		if (isNear(a2, 45, e)) {
			start = new Point2D.Float(0, 0);
			end = new Point2D.Float(normalize(end.getX(), width), normalize(end.getY(), height));
		}

		// near 90
		if (isNear(a2, 90, e)) {
			start = new Point2D.Float(0, 0);
			end = new Point2D.Float(0, normalize(end.getY(), height));
		}

		// near 135
		if (isNear(a2, 135, e)) {
			start = new Point2D.Float(normalize(start.getX(), width), 0);
			end = new Point2D.Float(0, normalize(end.getY(), height));
		}

		// near 180
		if (isNear(a2, 180, e)) {
			start = new Point2D.Float(normalize(start.getX(), width), 0);
			end = new Point2D.Float(0, 0);
		}

		// near 225
		if (isNear(a2, 225, e)) {
			start = new Point2D.Float(normalize(start.getX(), width), normalize(start.getY(), height));
			end = new Point2D.Float(0, 0);
		}

		// near 270
		if (isNear(a2, 270, e)) {
			start = new Point2D.Float(0, normalize(start.getY(), height));
			end = new Point2D.Float(0, 0);
		}

		// near 315
		if (isNear(a2, 315, e)) {
			start = new Point2D.Float(0, normalize(start.getY(), height));
			end = new Point2D.Float(normalize(end.getX(), width), 0);
		}

		return new Point2D[] { start, end };
	}

	private static boolean isNear(double angle, double target, double error) {
		return Math.abs(target - Math.abs(angle)) < error;
	}

	private static float normalize(double original, float target) {
		if (original < 1f) {
			return target * (float) original;
		}

		return target;
	}

	private static double calcAngle(Point2D p1, Point2D p2) {
		double x_off = p2.getX() - p1.getX();
		double y_off = p2.getY() - p1.getY();
		double angle = Math.atan(y_off / x_off);
		if (x_off < 0) {
			angle = angle + Math.PI;
		}

		if (angle < 0) {
			angle += 2 * Math.PI;
		}
		if (angle > 2 * Math.PI) {
			angle -= 2 * Math.PI;
		}
		return angle;
	}

	/*
	 * public static void main(String ... args) { LinearGradientPaint in = new
	 * LinearGradientPaint( new Point(0,0), new Point(10,0), new float[] {0f,
	 * 0.5f, 1f}, new Color[] {Color.RED, Color.GREEN, Color.BLUE});
	 * log.fine("in  = " + toString(in)); Paint out =
	 * resizeGradient(in,100,100); log.fine(("out = " +
	 * toString((MultipleGradientPaint) out)); }
	 */
	/*
	 * private static String toString(MultipleGradientPaint paint) {
	 * StringBuffer buffer = new StringBuffer();
	 * buffer.append(paint.getClass().getName()); Color[] colors =
	 * paint.getColors(); float[] values = paint.getFractions();
	 * buffer.append("["); for(int i=0; i<colors.length; i++) {
	 * buffer.append("#").append(Integer.toHexString(colors[i].getRGB()));
	 * buffer.append(":"); buffer.append(values[i]); buffer.append(", "); }
	 * buffer.append("]"); if(paint instanceof LinearGradientPaint) {
	 * LinearGradientPaint lgp = (LinearGradientPaint) paint;
	 * buffer.append(", "); buffer.append(""+lgp.getStartPoint().getX() + ", " +
	 * lgp.getStartPoint().getY()); buffer.append("->");
	 * buffer.append(""+lgp.getEndPoint().getX() + ", " +
	 * lgp.getEndPoint().getY()); }
	 * 
	 * return buffer.toString(); }
	 */

	/**
	 * Creates a new {@code Paint} that is a checkered effect using the colors
	 * {@link Color#GRAY gray} and {@link Color#WHITE}.
	 * 
	 * @return a the checkered paint
	 */
	public static Paint getCheckerPaint() {
		return getCheckerPaint(Color.WHITE, Color.GRAY, 20);
	}

	/**
	 * Creates a new {@code Paint} that is a checkered effect using the
	 * specified colors.
	 * <p>
	 * While this method supports transparent colors, this implementation
	 * performs painting operations using the second color after it performs
	 * operations using the first color. This means that to create a checkered
	 * paint with a fully-transparent color, you MUST specify that color first.
	 * 
	 * @param c1
	 *            the first color
	 * @param c2
	 *            the second color
	 * @param size
	 *            the size of the paint
	 * @return a new {@code Paint} checkering the supplied colors
	 */
	public static Paint getCheckerPaint(Paint c1, Paint c2, int size) {
		BufferedImage img = GraphicsUtilities.createCompatibleTranslucentImage(size, size);
		Graphics2D g = img.createGraphics();

		try {
			g.setPaint(c1);
			g.fillRect(0, 0, size, size);
			g.setPaint(c2);
			g.fillRect(0, 0, size / 2, size / 2);
			g.fillRect(size / 2, size / 2, size / 2, size / 2);
		} finally {
			g.dispose();
		}

		return new TexturePaint(img, new Rectangle(0, 0, size, size));
	}

	/**
	 * Creates a {@code String} that represents the supplied color as a
	 * hex-value RGB triplet, including the "#". The return value is suitable
	 * for use in HTML. The alpha (transparency) channel is neither include nor
	 * used in producing the string.
	 * 
	 * @param color
	 *            the color to convert
	 * @return the hex {@code String}
	 */
	public static String toHexString(Color color) {
		return "#" + Integer.toHexString(color.getRGB() | 0xFF000000).substring(2);
	}

	/**
	 * Returns a new color equal to the old one, except that there is no alpha
	 * (transparency) channel.
	 * <p>
	 * This method is a convenience and has the same effect as
	 * {@code setAlpha(color, 255)}.
	 * 
	 * @param color
	 *            the color to remove the alpha (transparency) from
	 * @return a new non-transparent {@code Color}
	 * @throws NullPointerException
	 *             if {@code color} is {@code null}
	 */
	public static Color removeAlpha(Color color) {
		return setAlpha(color, 255);
	}

	/**
	 * Returns a new color equal to the old one, except alpha (transparency)
	 * channel is set to the new value.
	 * 
	 * @param color
	 *            the color to modify
	 * @param alpha
	 *            the new alpha (transparency) level. Must be an int between 0
	 *            and 255
	 * @return a new alpha-applied {@code Color}
	 * @throws IllegalArgumentException
	 *             if {@code alpha} is not between 0 and 255 inclusive
	 * @throws NullPointerException
	 *             if {@code color} is {@code null}
	 */
	public static Color setAlpha(Color color, int alpha) {
		if (alpha < 0 || alpha > 255) {
			throw new IllegalArgumentException("invalid alpha value");
		}

		return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha);
	}

	/**
	 * Returns a new color equal to the old one, except the saturation is set to
	 * the new value. The new color will have the same alpha (transparency) as
	 * the original color.
	 * <p>
	 * The color is modified using HSB calculations. The saturation must be a
	 * float between 0 and 1. If 0 the resulting color will be gray. If 1 the
	 * resulting color will be the most saturated possible form of the passed in
	 * color.
	 * 
	 * @param color
	 *            the color to modify
	 * @param saturation
	 *            the saturation to use in the new color
	 * @return a new saturation-applied {@code Color}
	 * @throws IllegalArgumentException
	 *             if {@code saturation} is not between 0 and 1 inclusive
	 * @throws NullPointerException
	 *             if {@code color} is {@code null}
	 */
	public static Color setSaturation(Color color, float saturation) {
		if (saturation < 0f || saturation > 1f) {
			throw new IllegalArgumentException("invalid saturation value");
		}

		int alpha = color.getAlpha();

		float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null);
		Color c = Color.getHSBColor(hsb[0], saturation, hsb[2]);

		return setAlpha(c, alpha);
	}

	/**
	 * Returns a new color equal to the old one, except the brightness is set to
	 * the new value. The new color will have the same alpha (transparency) as
	 * the original color.
	 * <p>
	 * The color is modified using HSB calculations. The brightness must be a
	 * float between 0 and 1. If 0 the resulting color will be black. If 1 the
	 * resulting color will be the brightest possible form of the passed in
	 * color.
	 * 
	 * @param color
	 *            the color to modify
	 * @param brightness
	 *            the brightness to use in the new color
	 * @return a new brightness-applied {@code Color}
	 * @throws IllegalArgumentException
	 *             if {@code brightness} is not between 0 and 1 inclusive
	 * @throws NullPointerException
	 *             if {@code color} is {@code null}
	 */
	public static Color setBrightness(Color color, float brightness) {
		if (brightness < 0f || brightness > 1f) {
			throw new IllegalArgumentException("invalid brightness value");
		}

		int alpha = color.getAlpha();

		float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null);
		Color c = Color.getHSBColor(hsb[0], hsb[1], brightness);

		return setAlpha(c, alpha);
	}

	/**
	 * Blends two colors to create a new color. The {@code origin} color is the
	 * base for the new color and regardless of its alpha component, it is
	 * treated as fully opaque (alpha 255).
	 * 
	 * @param origin
	 *            the base of the new color
	 * @param over
	 *            the alpha-enabled color to add to the {@code origin} color
	 * @return a new color comprised of the {@code origin} and {@code over}
	 *         colors
	 */
	public static Color blend(Color origin, Color over) {
		if (over == null) {
			return origin;
		}

		if (origin == null) {
			return over;
		}

		int a = over.getAlpha();

		int rb = (((over.getRGB() & 0x00ff00ff) * (a + 1)) + ((origin.getRGB() & 0x00ff00ff) * (0xff - a))) & 0xff00ff00;
		int g = (((over.getRGB() & 0x0000ff00) * (a + 1)) + ((origin.getRGB() & 0x0000ff00) * (0xff - a))) & 0x00ff0000;

		return new Color((over.getRGB() & 0xff000000) | ((rb | g) >> 8));
	}

	/**
	 * Interpolates a color.
	 * 
	 * @param b
	 *            the first color
	 * @param a
	 *            the second color
	 * @param t
	 *            the amount to interpolate
	 * @return a new color
	 */
	public static Color interpolate(Color b, Color a, float t) {
		float[] acomp = a.getRGBComponents(null);
		float[] bcomp = b.getRGBComponents(null);
		float[] ccomp = new float[4];

		// log.fine(("a comp ");
		// for(float f : acomp) {
		// log.fine((f);
		// }
		// for(float f : bcomp) {
		// log.fine((f);
		// }
		for (int i = 0; i < 4; i++) {
			ccomp[i] = acomp[i] + (bcomp[i] - acomp[i]) * t;
		}
		// for(float f : ccomp) {
		// log.fine((f);
		// }

		return new Color(ccomp[0], ccomp[1], ccomp[2], ccomp[3]);
	}

	/**
	 * Computes an appropriate foreground color (either white or black) for the
	 * given background color.
	 * 
	 * @param bg
	 *            the background color
	 * @return {@code Color.WHITE} or {@code Color.BLACK}
	 * @throws NullPointerException
	 *             if {@code bg} is {@code null}
	 */
	public static Color computeForeground(Color bg) {
		float[] rgb = bg.getRGBColorComponents(null);
		float y = .3f * rgb[0] + .59f * rgb[1] + .11f * rgb[2];

		return y > .5f ? Color.BLACK : Color.WHITE;
	}
}
